package com.jse.tpl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;

import com.jse.Fs;
import com.jse.Io;
import com.jse.Js;

/**
 * 基本用法: <code>String re = Jst.create(tpl).render(ctx);</code>
 * @author wendal(wendal1985@gmail.com)
 *
 */
public class Jst {

    protected List<JstBlock> jstBlocks = new ArrayList<JstBlock>();
    protected ArrayList<String> strs = new ArrayList<String>();
    protected ArrayList<String> lines = new ArrayList<String>();
    protected String jsStr;
    protected ScriptEngine engine;
    protected CompiledScript compiledScript;
    protected String name = "<jst>";
    public String source;
    protected boolean debug=false;
    protected String singleLineJs = "#";
    protected String[] multiLineJs = new String[] {"<!--#", "#-->"};
    private static Jst jst = new Jst();
    
    private Jst() {
        this.engine = Js.js;
    }

    public Jst(String str) {
        this();
        this.source = str;
        compile();
    }

    public Jst(Reader r) {
        this(Io.read(r));
    }

    protected void parse(BufferedReader br) {
        try {
            String line = null;
            int lineNumber = -1;
            while ((line = br.readLine()) != null) {
                lineNumber++;
                lines.add(line);
                String _line = line.trim();
                if (_line.startsWith(singleLineJs)) {
                    JstStatmentBlock block = new JstStatmentBlock();
                    block.lineNumber = lineNumber;
                    block.str = line.substring(line.indexOf(singleLineJs)+1);
                    jstBlocks.add(block);
                    continue;
                }
                if (_line.startsWith(multiLineJs[0])) {
                    JstStatmentBlock block = new JstStatmentBlock();
                    block.lineNumber = lineNumber;
                    jstBlocks.add(block);
                    if (_line.endsWith(multiLineJs[1])) {
                        block.str = line.substring(line.indexOf(multiLineJs[0])+multiLineJs[0].length(), line.lastIndexOf(multiLineJs[1]));
                        continue;
                    }
                    else {
                        block.str = line.substring(line.indexOf(multiLineJs[0])+multiLineJs[0].length());
                    }
                    while ((line = br.readLine()) != null) {
                        lineNumber++;
                        lines.add(line);
                        _line = line.trim();
                        block = new JstStatmentBlock();
                        block.lineNumber = lineNumber;
                        jstBlocks.add(block);
                        block.str = line;
                        if (_line.endsWith(multiLineJs[1])) {
                            block.str = line.substring(0, line.lastIndexOf(multiLineJs[1]));
                            break;
                        }
                    }
                    continue;
                }
                if (line.contains("${")) {
                    int start = 0;
                    while (true) {
                        int new_start = line.indexOf("${", start);
                        if (new_start == -1) {
                            if (line.length() > start) {
                                addStaticBlock(line.substring(start), lineNumber);
                            }
                            addStaticBlock("\r\n", lineNumber);
                            break;
                        }
                        // 如果是 $${ 跳过解析
                        if (new_start > 0 && line.charAt(new_start - 1) == '$') {
                            addStaticBlock(line.substring(start, new_start) + "{", lineNumber);
                            start = new_start + 2;
                            continue;
                        }
                        if (new_start > start) {
                            addStaticBlock(line.substring(start, new_start), lineNumber);
                        }
                        int end = line.indexOf("}", new_start);
                        if (end < 1) {
                            throw new RuntimeException("miss } at line " + lineNumber);
                        }
                        jstBlocks.add(new JstEvalBlock(line.substring(new_start + 2, end), lineNumber));
                        start = end + 1;
                    }
                } else {
                    addStaticBlock(line + "\r\n", lineNumber);
                }
            }
        }
        catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    protected void addStaticBlock(String str, int lineNumber) {
        if (jstBlocks.isEmpty()) {
            jstBlocks.add(new JstStaticBlock(str, lineNumber));
            return;
        }
        JstBlock jstBlock = jstBlocks.get(jstBlocks.size() - 1);
        if (jstBlock instanceof JstStaticBlock && ((JstStaticBlock) jstBlock).lineNumber == lineNumber) {
            ((JstStaticBlock) jstBlock).str += str;
            return;
        }
        jstBlocks.add(new JstStaticBlock(str, lineNumber));
    }

    /**
     * 生成js文本并编译,如果已经生成过,则直接返回
     */
    public String compile() {
        return compile(false);
    }

    /**
     * 生成js文本并编译
     * @param force 是否强制重新生成
     */
    public String compile(boolean force) {
        if (force || jsStr == null) {
            parse(new BufferedReader(new StringReader(source)));
            StringBuilder sb = new StringBuilder();
            sb.append("var $f = function(){");
            JstBlock[] blocks = this.jstBlocks.toArray(new JstBlock[this.jstBlocks.size()]);
            JstBlock prev = null;
            for (int i = 0; i < blocks.length; i++) {
                JstBlock jstBlock = blocks[i];
                if (prev != null && prev.lineNumber != jstBlock.lineNumber) {
                    sb.append("\r\n");
                }
                if (jstBlock instanceof JstStaticBlock) {
                    sb.append("$out.write($strs[").append(strs.size()).append("]);");
                    strs.add(jstBlock.str);
                } else {
                    jstBlock.asJs(sb);
                }
                prev = jstBlock;
            }
            sb.append("};$f();");
            jsStr = sb.toString();
        }
        if (force || compiledScript == null) {
            try {
                compiledScript = ((Compilable) engine).compile(jsStr);
            }
            catch (ScriptException e) {
                throw new RuntimeException(e);
            }
        }
        return jsStr;
    }

    /**
     * 渲染并写入Writer
     */
    public Object render(Map<String,Object> ctx, Writer w) {
        try {
            ScriptContext newContext = new SimpleScriptContext();
            Bindings bindings = newContext.getBindings(ScriptContext.ENGINE_SCOPE);
            bindings.putAll(ctx);
            bindings.put("$out", w);
            bindings.put("$strs", strs);
            newContext.setAttribute(ScriptEngine.FILENAME, name, ScriptContext.ENGINE_SCOPE);
            if (debug || compiledScript == null)
                return engine.eval(jsStr, newContext);
            return compiledScript.eval(newContext);
        }
        catch (ScriptException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 使用ctx作为上下文渲染,并返回字符串
     */
    public String render(Map<String,Object> ctx) {
        StringWriter w = new StringWriter();
        render(ctx, w);
        return w.toString();
    }

    /**
     * 根据ScriptException生成带模板源码的报错信息
     */
    protected RuntimeException makeException(ScriptException e) {
        int lineNumber = e.getLineNumber();
        StringBuilder sb = new StringBuilder(e.getMessage());
        for (int i = lineNumber - 2; i <= lineNumber + 2; i++) {
            if (i < 1)
                continue;
            if (i > lines.size())
                break;
            sb.append(String.format("\r\n%3d %s %s", i, i == lineNumber ? '>' : ':', lines.get(i - 1)));
        }
        String msg = sb.toString();
        Throwable cause = e.getCause();
        if (cause == null)
            return new RuntimeException(msg);
        return new RuntimeException(msg, cause);
    }

    /**
     * 获取内部Block序列
     */
    public List<JstBlock> getBlocks() {
        return jstBlocks;
    }

    /**
     * 设置文件名,在debug模式下能打印到报错信息中
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 设置debug模式,默认是false
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }
    
    /**
     * 为当前模板独立设置一个引擎
     * @param engine
     */
    public void setEngine(ScriptEngine engine) {
        this.engine = engine;
    }
    
    public static Jst createFromFile(String f) {
        return new Jst(Fs.readString(f));
    }
    public static Jst create(String code) {
        return new Jst(code);
    }
    
    public static String renderToString(String source, Map<String,Object> ctx) {
    	jst.source=source;
    	jst.compile();
        return jst.render(ctx);
    }
    public static Object render(String source, Map<String,Object> ctx,Writer w) {
    	jst.source=source;
    	jst.compile();
        return jst.render(ctx,w);
    }
    
    private abstract class JstBlock {
        public String str;
        public int lineNumber;

        public abstract void asJs(StringBuilder sb);
    }
    
    private class JstEvalBlock extends JstBlock {
        protected boolean htmlSafe;
        public JstEvalBlock(String str, int lineNumber) {
            if (str.startsWith("=")) {
                htmlSafe = true;
                str = str.substring(1);
            }
            this.str = str;
            this.lineNumber = lineNumber;
        }
        public void asJs(StringBuilder sb) {
            sb.append("$out.write(''+(");
            if (htmlSafe)
                sb.append("com.jse.util.Strings.escapeHtml(''+(");
            sb.append(str);
            if (htmlSafe)
                sb.append("))");
            sb.append("));");
        }
    }
    
    private class JstStaticBlock extends JstBlock {
        public JstStaticBlock(String str, int lineNumber) {
            this.str = str;
            this.lineNumber = lineNumber;
        }
        public void asJs(StringBuilder sb) {}
    }
    
    private class JstStatmentBlock extends JstBlock {
        @Override
        public void asJs(StringBuilder sb) {
            sb.append(str);
        }
    }
}
